雖然我們學到 ORB 能自動找到數十甚至上百對的匹配點,但其中不可避免地會包含一些錯誤的匹配。如果我們把這些包含「雜訊」的匹配點全部丟進去計算單應性矩陣,結果可能相當不好,因此隨機樣本共識 (RANdom SAmple Consensus, RANSAC) 由此誕生。
RANSAC 的核心思想是一種迭代式的投票機制,它的想法是「假設一小部分數據是乾淨的,用它們建立一個模型,然後看看有多少其他數據支持這個模型」。流程基本如下
隨機採樣 (RANdom SAmple):從所有的匹配點中,隨機選取計算模型所需的最小樣本數量。對於單應性矩陣 H,這個數量是 4 對匹配點。
計算模型:使用這 4 對點,計算出一個候選的單應性矩陣 H。
驗證與共識 (consensus):將所有的匹配點,都用這個候選的 H 進行變換,然後計算其與實際對應點之間的誤差(重投影誤差)。如果誤差小於某個閾值,我們就認為這個點是一個「內點」(inliers),即它「支持」或「同意」這個模型。統計出支持這個模型 H 的內點總數。
迭代:重複步驟 1-3 多次(例如 100 次)。
選出最佳模型:在所有迭代中,那個獲得最多內點支持的單應性矩陣 H,就被認為是最佳模型。
RANSAC 的優點在於,只要內點的比例足夠高,它就有極大的機率在某次迭代中,恰好選中一組全部由內點構成的樣本,從而找到正確的模型,完全不受外點 (outliers) 的干擾。
全景拼接 (panorama stitching) 就是將多張有重疊區域的圖片,無縫地拼接成一張更寬廣的圖片。有了 ORB 特徵匹配和 RANSAC 穩健估計,我們現在擁有實現它所需的所有工具。
完整的處理流程如下
讀取兩張有重疊區域的圖片(例如,一張偏左,一張偏右)。
使用 ORB 在兩張圖片中分別偵測特徵點並計算描述子。
使用 Brute-Force 匹配器找到兩組描述子之間的對應關係。
將匹配好的點對座標傳入 cv2.findHomography 函式。這個函式內部就實現了 RANSAC,它會自動從大量匹配中,穩健地計算出最佳的單應性矩陣 H。
有了矩陣 H,我們使用 cv2.warpPerspective 函式,將其中一張圖片進行透視變換,使其「扭曲」到與另一張圖片相同的視角平面上。
最後,建立一個足夠大的畫布,將兩張圖片(一張是原始的,一張是變換後的)拼接在一起。
import cv2
import numpy as np
def stitch_images(img1, img2, min_match_count=10):
"""
使用 ORB 和 RANSAC 拼接兩張圖片。
img1: 右側圖片
img2: 左側圖片
"""
# --- 1. 偵測特徵並匹配 ---
orb = cv2.ORB_create(nfeatures=2000)
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(des1, des2)
matches = sorted(matches, key=lambda x: x.distance)
# --- 2. 使用 RANSAC 計算單應性矩陣 ---
if len(matches) > min_match_count:
# 提取匹配點的座標
src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
# cv2.findHomography(src, dst, method, ransacReprojThreshold)
# method=cv2.RANSAC: 指定使用 RANSAC 演算法
# ransacReprojThreshold: 重投影誤差閾值,通常設為 4.0 或 5.0
# M: 計算出的 3x3 單應性矩陣
# mask: 標記了內點(1)和外點(0)
M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
# --- 3. 應用透視變換 ---
h1, w1 = img1.shape[:2]
h2, w2 = img2.shape[:2]
# 將 img1 進行變換,使其與 img2 對齊
# 輸出畫布的大小設定為兩張圖的總寬度和較高的那個高度
warped_img = cv2.warpPerspective(img1, M, (w1 + w2, max(h1, h2)))
# --- 4. 拼接圖片 ---
# 將 img2 (左側圖) 放置在畫布的左側
# 然後將變換後的 img1 (右側圖) 疊加在上面
result = warped_img.copy()
result[0:h2, 0:w2] = img2
# (可選) 移除右側的黑色邊界
# 找到第一個非黑色的列
gray_result = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray_result, 1, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
cnt = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
result = result[y:y+h, x:x+w]
return result
else:
print(f"Not enough matches are found - {len(matches)}/{min_match_count}")
return None
# --- 主程式 ---
if __name__ == '__main__':
# 讀取圖片 (img_right 是右邊的圖, img_left 是左邊的圖)
img_right = cv2.imread('panorama2.jpg')
img_left = cv2.imread('panorama1.jpg')
if img_right is None or img_left is None:
print("圖片讀取失敗!請檢查路徑。")
else:
stitched_result = stitch_images(img_right, img_left)
if stitched_result is not None:
cv2.imshow('Stitched Panorama', stitched_result)
cv2.waitKey(0)
cv2.imwrite("panorama.jpg", stitched_result)
cv2.destroyAllWindows()
原圖
合成後